package IMDB;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransportException;
import org.ini4j.Ini;
import org.ini4j.InvalidFileFormatException;

import KVService.KVClientStandAlone;
import KVService.KVClientStandAlone.Tests;
import Node.Node;
import Node.TMultiplexedProtocol;
import RMService.*;
import QueryService.*;

/**
 * IMDB Client class.
 *
 */
public class IMDBClient {

	// Member variables
	private ArrayList<RMService.Client> rmClients;
	private ArrayList<QueryService.Client> queryClients;
	private Random randomGenerator;
	private String imdbFile;
	
	// Enumeration for tests
	public enum Tests {
		INSERTIMDBDATA,
		LOOKUPLATENCY,
		QUERY,
		INSERT,
		GET,
		DELETE
	}
	
	/**
	 * Constructor for the class.
	 * 
	 * @param hosts arraylist of hosts 
	 * @throws TTransportException
	 */
	public IMDBClient (ArrayList<String[]> hosts) throws TTransportException {
		// Create client for each kv server
		rmClients = new ArrayList<RMService.Client> ();
		
		// Create a query client for each KV Server
		queryClients = new ArrayList<QueryService.Client> ();
		
		// Loop over the hosts
		for (String[] host : hosts) {
			
			// Create a new socket
			TSocket socket = new TSocket(host[0], Integer.parseInt(host[1]));
			
			// Open the socket
			try{
				socket.open();
			} catch (Exception e){
				// topology may have changed, continue connecting to other nodes in topology
				continue;
			}
		
			// Initialize clients protocol: JSON, compact, binary, etc.
			TBinaryProtocol protocol = new TBinaryProtocol(socket);

			// Register service (service name must match service name registered on server) 
			TMultiplexedProtocol rmMP = new TMultiplexedProtocol(protocol, "RMService");
			TMultiplexedProtocol queryMP = new TMultiplexedProtocol(protocol, "QueryService");
			
			// Add the RMService client to the list of clients
			rmClients.add(new RMService.Client(rmMP));
			
			
			// Add the QueryService client to the list of query clients
			queryClients.add(new QueryService.Client(queryMP));
			
			// Initialize the randomGenerator
			randomGenerator = new Random();
			
			// Set the path to the IMDB data file
			// TODO: hard-coded data path to file
			this.imdbFile = "../../../data/unique_movies_list.txt";
		}
	}
	
	public void testInsertIMDBData(int total) throws TException {
		System.out.println("Inserting IMDB data from file: " + this.imdbFile);
		
		// Member variables
		FileWriter fStream = null;
		BufferedWriter out = null;
		
		// Try to create the output data file
		try {
			fStream = new FileWriter("insert_latency_out.txt");
			out = new BufferedWriter(fStream);
			out.write("Lookup_Key|Movie_Title|Initiator_Node|Lookup_Node|Elapsed_Time\n");
		}
		catch (Exception e) {
			System.err.println("Error opening file: " + e.getMessage());
		}
		
		try {
			// Open the IMDB data file
			FileInputStream fstream = new FileInputStream(this.imdbFile);
			// Get the object of DataInputStream
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in));
			
			String strLine;
			int key;
			
			// Read the file line by line
			int i = 0;
			while ((strLine = br.readLine()) != null && i < total)
			{		
				// Make the key be the hash value of the movie title
				key = strLine.hashCode();
				
				// Generate random node to issue insert from
				int index = randomGenerator.nextInt(rmClients.size());
				RMService.Client randClient = rmClients.get(index);
				
				// Perform lookup operation (lookup node, insert key value)
				long start = System.nanoTime();
				int node = randClient.lookup(key);
				RMService.Client keyClient = rmClients.get(node);
				keyClient.insert(key, strLine);
				long elapsedTime = System.nanoTime() - start;
				System.out.println("Insert key: " + key + ", title: " + strLine + ", at node: " + node + ", time: " + elapsedTime);
				
				// Output csv-style data to file
				try {
					if (out != null) {
						out.write(key + "|" + strLine + "|" + index + "|" + node + "|" + elapsedTime + "\n");
						out.flush();
					}
				}
				catch (Exception e) {
					System.err.println("Error writing: " + e.getMessage());
				}	
				i++;
			}
			
			// Close the input stream
			in.close();					
		}
		catch (IOException e) {
			System.out.println("Error: " + e.getMessage());			
		}
	}
	
	public void testLookupLatency() throws TException {
		System.out.println("Looking up node owners for IMDB data from file: " + this.imdbFile);
		
		// Member variables
		FileWriter fStream = null;
		BufferedWriter out = null;
		
		// Try to create the output data file
		try {
			fStream = new FileWriter("lookup_latency_out.txt");
			out = new BufferedWriter(fStream);
			out.write("Lookup_Key|Movie_Title|Initiator_Node|Lookup_Node|Elapsed_Time\n");
		}
		catch (Exception e) {
			System.err.println("Error opening file: " + e.getMessage());
		}
		
		try {
			// Open the IMDB data file
			FileInputStream fstream = new FileInputStream(this.imdbFile);
			// Get the object of DataInputStream
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in));
			
			String strLine;
			int key;
			
			// Read the file line by line
			while ((strLine = br.readLine()) != null)
			{		
				// Make the key be the hash value of the movie title
				key = strLine.hashCode();
				
				// Generate random node to issue insert from
				int index = randomGenerator.nextInt(rmClients.size());
				RMService.Client randClient = rmClients.get(index);
				
				// Perform lookup operation (lookup node, insert key value)
				long start = System.nanoTime();
				int node = randClient.lookup(key);
				long elapsedTime = System.nanoTime() - start;
				System.out.println("Lookup key: " + key + ", title: " + strLine + ", at node: " + node + ", time: " + elapsedTime);
				
				// Output csv-style data to file
				try {
					if (out != null) {
						out.write(key + "|" + strLine + "|" + index + "|" + node + "|" + elapsedTime + "\n");
						out.flush();
					}
				}
				catch (Exception e) {
					System.err.println("Error writing: " + e.getMessage());
				}	
			}
			
			// Close the input stream
			in.close();					
		}
		catch (IOException e) {
			System.out.println("Error: " + e.getMessage());			
		}
	}
	
	public void testMoviesContaining(String keyword) {
		System.out.println("Looking up movie titles containing the word: '" + keyword + "'");
		
		// Member variables
		List<String> movieTitlesMatching = new ArrayList<String>();
		Map<Integer, String> titlesMatching = new HashMap<Integer, String>();
		
		// Generate random node to issue insert from
		int index = randomGenerator.nextInt(queryClients.size());
		QueryService.Client randClient = queryClients.get(index);
		
		// Perform get matching movie titles operation (on provided keyword)
		long start = System.nanoTime();
		try 
		{
			movieTitlesMatching = randClient.getKeysContainingWord(keyword, titlesMatching, -1, queryClients.size());
		} 
		catch (TException e) {
			System.out.println("Exception occurred while getting matching movie titles: " + e);
			e.printStackTrace();
		}
		long elapsedTime = System.nanoTime() - start;
		System.out.println("Get matching movie titles for word '" + keyword + "' took: " + elapsedTime + " (ns)");
		
		// TODO: Print out the matching movie titles
		System.out.println("Found " + movieTitlesMatching.size() + " matching movie titles!");
		for(String title : movieTitlesMatching){
			System.out.println("\t " + title);
		}
	}
	
	public void insert(int node, String value) throws TException {
		
		System.out.println("Insert (value: " + value + ") at node " + node);
		RMService.Client client = rmClients.get(node);
		
		int key = value.hashCode();
		
		// get key
		node = client.lookup(key);
		RMService.Client keyClient = rmClients.get(node);
		System.out.println("Insert found key " + key + " at node " + node);
		keyClient.insert(key, value);
		System.out.println("Insert added ( "+ key +" ," + value +" ) pair to node " + node);
	}
	
	public void get(int node, int key) throws TException {
		
		System.out.println("Get key " + key +" at node " + node);
		RMService.Client client = rmClients.get(node);
		
		// get key
		node = client.lookup(key);
		RMService.Client keyClient = rmClients.get(node);
		System.out.println("Get found key " + key + " at node " + node);
		String data = keyClient.get(key);
		System.out.println("Get retrieved data: "+ data);
	}
	
	public void delete(int node, int key) throws TException {
		
		System.out.println("Delete key " + key +" at node " + node);
		RMService.Client client = rmClients.get(node);
		
		// get key
		node = client.lookup(key);
		RMService.Client keyClient = rmClients.get(node);
		System.out.println("Delete found key " + key + " at node " + node);
		keyClient.deleteKey(key);
		System.out.println("Key Deleted");
	}
	
	/**
	 * Main
	 * 
	 * @param args
	 */
	public static void main(String[] args) throws TException, InvalidFileFormatException, FileNotFoundException, IOException, InterruptedException {
		
		// Check for the correct number of arguments to be passed in
	    if (args.length < 2) {
	        // Print out an error message to the screen
	        System.out.println("Error: Incorrect number of input parameters!");
	        // Call function to display usage
	        DisplayUsage();
	        // Exit the program
	        System.exit(0);
	    }
	    
	    // Create an ini object for the topology information
	    Ini topologyIni = new Ini();
	    topologyIni.load(new FileReader(args[0]));
	    
	    // Parse the addresses section from the topology ini file
        Ini.Section section = topologyIni.get("Nodes");
        String addresses = section.get("Addresses");
        ArrayList<String[]> hosts = Node.ParseAddresses(addresses);
        IMDBClient testDriver = new IMDBClient(hosts);
        
        // Run test
        Tests test = Tests.valueOf(args[1].toUpperCase());
        
        // Switch over which test to run
        switch (test) {
	        case INSERTIMDBDATA:
	            testDriver.testInsertIMDBData(Integer.parseInt(args[2]));
	            break;
	        case LOOKUPLATENCY:
	        	testDriver.testLookupLatency();
	        	break;
	        case QUERY:
	        	String query = "";
	        	// combine args to form query
	        	for(int i = 2; i < args.length; i++){
	        		query += " " + args[i];
	        	}
	        	testDriver.testMoviesContaining(query);
	        	break;
	        case INSERT:
	        	// combine args to form insert
	        	String value = "";
	        	System.out.println(args.length);
	        	for(int i = 3; i < args.length; i++){
	        		value += " " + args[i];
	        	}
	        	testDriver.insert(Integer.parseInt(args[2]), value);
	            break;
	        case GET:
	            testDriver.get(Integer.parseInt(args[2]), Integer.parseInt(args[3]));
	            break;
	        case DELETE:
	            testDriver.delete(Integer.parseInt(args[2]), Integer.parseInt(args[3]));
	            break;
	        default:
	        	System.out.println("Test " + args[1] + " does not exist");    	
        }
	}
	
	/**
	 * Function to display the usage
	 */
	public static void DisplayUsage() {
	    System.out.println("\nIMDBClient - Used to launch an IMDB client\n");
	    System.out.println("\tUsage: ./IMDBClient <topology-ini> <test-name> [<node> <key> <value>]");
	}
}
